from webob import Request, Response
from webob.exc import HTTPUnauthorized
import pkg_resources

from authority.utils import load_ep_from_group

import logging
log = logging.getLogger(__name__)

# should use setuptools to dynamically load this at some point
from authority.schemes import HTTPBasicMatchScheme, AuthFormMatchScheme, DynamicScheme

from pprint import pformat, pprint

'''
When request comes in, one of the following can be true:
- user has not been authenticated, no credentials in request
- user has not been authenticated, but credentials are included with request
- user has been authenticated

'''

class AuthorityMiddleware(object):
    '''Authority middleware object
    
    This object wraps the protected application and provides it 
    with authentication and/or authorization services.
    
    Configuration options are loaded from a config dictionary 
    (typically provided through a Paste config file), and optionally 
    through keyword arguments provided when the middleware object is 
    created (when the __init__ function is called). Keyword config 
    options will override those passed in the config dict.
    
    Config options are recognized by including the prefix "authority.",
    so any options beginning with "authority." in a Paste config file 
    should be loaded by Authority.
    
    Similarly, some options are passed on to the various components of 
    Authority, as follows:
        "authority.method."   -- passed to the method object
        "authority.checker."  -- passed to the checker object
        "authority.provider." -- passed to the provider object
    '''

    def _loadConfigOptions(self, optionConfig=None, **kwargs):
        '''Load configuration options for Authority.
        
        This function is used internally and should never need to be 
        called by client code.
        '''
        optionConfig = optionConfig or {}

        config = {}

        # set up defaults here
        options = dict(always_refresh=True, max_attempts=3)

        options['method_opts']   = {}
        options['checker_opts']  = {}
        options['provider_opts'] = {}
        options['scheme'] = None

        config.update(optionConfig)
        config.update(kwargs)

        # load config stuff here
        iterator = ((k[10:], v) for k, v in config.iteritems() if k.startswith('authority.'))

        for key, val in iterator:
            split = key.split('.')
            if split[0] in ('method', 'checker', 'provider'):
                key1 = split[0]+'_opts'
                key2 = '.'.join(split[1:])
                if key2 == '':
                    options[split[0]] = val
                else:
                    options[key1][key2] = val
                continue
            options[key] = val
        return options

    def __init__(self, app, config=None, **kwargs):
        '''Initialize the middleware object.

        '''
        self.options = self._loadConfigOptions(config, **kwargs)
        # maybe validate the config options here

        log.debug('config options: \n%s', pformat(self.options))

        self.app = app
        
        self.scheme = self.get_scheme()
        self.max_failed_logins = self.options['max_attempts']

        #~ self.app = app


    def get_scheme(self):
        '''Get an Authority Scheme based on configuration information
        
        if "authority.scheme" is set in the config, get the corresponding
        scheme using setuptools
        
        if "authority.scheme" is set to 'auto', 
        dynamically create a scheme based on
            self.options['method']
            self.options['checker']
            self.options['provider']
        '''
        
        if self.options['scheme'] == 'auto':
            for key in ('method', 'checker', 'provider'):
                if not self.options.has_key(key):
                    self.options[key] = None
            scheme = DynamicScheme(options=self.options)
            scheme.app = self.app
            return scheme
            #~ return DynamicScheme(app, options=self.options)

        # load scheme class based on self.options['scheme']
        dist = 'Authority'
        group = 'authority.schemes'
        name = self.options['scheme']

        try:
            log.debug('attempting to load scheme %s' % name)
            scheme_class = load_ep_from_group('authority.schemes', name)
            #~ scheme_class = pkg_resources.load_entry_point(dist, group, name)
            log.debug('loaded scheme class: %s' % scheme_class.__name__)
        except ImportError:
            log.warning('failed to find scheme: %s' % name)
            log.warning('using default scheme: AuthFormMatch')
            scheme_class = AuthFormMatchScheme

        scheme = scheme_class(options=self.options)
        scheme.app = self.app

        #~ return scheme_class(self.app, options=self.options)
        return scheme

    def __call__(self, environ, start_response):
        '''

        '''
        # start beaker session
        session_key = 'beaker.session'
        session = None
        if session_key in environ:
            session = environ[session_key]
            log.debug('Session started')

        if session != None:
            #refresh user object if required
            if not session.has_key('authority.user'):
                # user does not exist, so user is not authenticated yet
                log.debug('User has not been authenticated')
                # get a new user object
                session['authority.user'] = self.scheme.get_user()
                session.save()
            elif self.options['always_refresh']:
                # refresh the user object here
                #~ log.debug('Refreshing user object')
                pass
                # otherwise, don't refresh user obj

        req = Request(environ)
        resp = req.get_response(self.app)

        if resp.status_int == 401:
            log.debug('Page is unauthorized')

            #Check if user credentials are in the request
            credentials = self.scheme.get_credentials(environ)
            if not credentials:
                log.debug('No Credentials found in request')
                prompt = self.scheme.prompt_user()
                log.debug('Prompting user for credentials')
                # prompt is a WSGI app
                return prompt(environ, start_response)

            log.debug('Credentials found in request')
            log.debug(credentials)
            if not self.scheme.authenticate(credentials):
                # if we haven't exceeded the number of max logins
                #~ if session['authority.failed_attempts'] <= self.max_failed_logins:
                log.debug('Authentication failed, prompting again')
                prompt = self.scheme.prompt_user()
                return prompt(environ, start_response)
                #~ else:

            # then user has authenticated
            log.debug('authentication succeeded...what now?')
            #~ mark user as authenticated in session
            if session != None:
                user = session['authority.user']
                user.name = credentials['user']
                session.save()
            environ["REMOTE_USER"] = credentials['user']

            #now get the original response obj
            resp = req.get_response(self.app)
            return resp(environ, start_response)


        return resp(environ, start_response)